package chai.ningning.foundation.jdbc;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.logging.Logger;

public final class JdbcHelper {

    private static final Logger logger = Logger.getLogger( JdbcHelper.class.getName() );
    private static final String configFileName = "jdbc.properties" ; // 指定配置文件的名称

    private String driver ; // 驱动类
    private String url ; // 数据库连接地址
    private String user ; // 数据库用户名
    private String password ; // 数据库密码

    // 声明一个Connection类型的实例变量用来缓存数据库连接对象
    private Connection connection ;

    private JdbcHelper(){
        super();
    }

    public static JdbcHelper getInstance( DatabaseType type ) {
        // 创建一个JdbcTools实例
        JdbcHelper instance = new JdbcHelper();
        instance.read( type ); // 根据数据库类型读取配置文件中的内容
        instance.load(); // 加载驱动
        instance.connect(); // 获得连接
        return instance ;
    }

    private final StringBuffer buffer = new StringBuffer();

    private String toKey( String dbname , String suffix ) {
        buffer.setLength( 0 );
        buffer.append( "jdbc.connection." );
        buffer.append( dbname );
        buffer.append( suffix );
        return buffer.toString();
    }

    private void read( DatabaseType type ) {
        String dbname = type.name().toLowerCase() ;
        String driverKey = this.toKey( dbname , ".driver" );
        String urlKey = this.toKey( dbname , ".url" );
        String userKey = this.toKey( dbname , ".user" );
        String passwordKey = this.toKey( dbname , ".password" );

        Properties props = new Properties();

        Class<?> c = JdbcHelper.class ;
        // 注意通过 Class实例 的 getResourceAsStream 读取 类路径(classpath) 下指定文件
        InputStream in = c.getResourceAsStream( "/" + configFileName );
        try {
            props.load( in );
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        driver = props.getProperty( driverKey );
        url = props.getProperty( urlKey );
        user = props.getProperty( userKey );
        password = props.getProperty( passwordKey );
    }

    // 负责加载驱动的方法
    private void load(){
        try {
            Class.forName( driver );
            logger.info( "驱动" + driver + "加载成功" );
        } catch (ClassNotFoundException e) {
            logger.info( "驱动" + driver + "加载失败" );
            e.printStackTrace();
        }
    }

    /**
     * 判断 connection 变量是否引用了【无效】的数据库连接
     * @return 若 connection 为 null 或 引用了无效的数据库连接对象 则返回true
     */
    private boolean invalid(){
        if( connection == null ) { // 如果 connection 变量为 null
            return true ; // 则认为 connection 就是无效的
        }

        try {
            // connection.isValid( timeout ) 可以在指定的时间里检查连接是否是有效的
            return !connection.isValid(5); // 注意表达式中的感叹号
        } catch ( SQLException ex ) {
            return false ;
        }
    }

    // 专门负责连接数据库
    private void connect(){
        // 检查现有的connection是否引用了有效的连接对象
        if( this.invalid() ) {
            try {
                this.connection = DriverManager.getConnection(url, user, password);
            } catch (SQLException ex) {
                throw new RuntimeException("数据库连接失败", ex);
            }
        }
    }

    /**
     * 根据参数指定的SQL语句创建相应的 PreparedStatement实例
     * @param sql
     * @return
     */
    private PreparedStatement prepare(String sql ){
        try {
            return connection.prepareStatement( sql );
        } catch (SQLException ex) {
            throw new RuntimeException( "创建Statement失败" , ex );
        }
    }

    /**
     * 专门为指定的 PreparedStatement实例 设置 参数占位符 的 参数值
     * @param ps
     * @param params
     */
    private void setParameter( PreparedStatement ps , Object... params ){
        if( Objects.nonNull( ps ) && Objects.nonNull( params ) ) {
            try {
                for (int paramIndex = 1; paramIndex <= params.length; paramIndex++) {
                    int index = paramIndex - 1;
                    ps.setObject(paramIndex, params[index]);
                }
            } catch ( SQLException ex ) {
                ex.printStackTrace();
            }
        }
    }

    /**
     * 专门负责执行 DML 语句 ( INSERT / UPDATE / DELETE )
     * @param sql 表示被执行的DML语句 (可能带有参数占位符)
     * @param params 表示参数占位符的值(如果第一个参数对应的SQL中有参数占位符的话)
     * @return 返回受SQL影响的记录数目
     */
    public int update( String sql , Object... params ) throws DataAccessException {
        // 根据 参数传入的SQL语句 创建 PreparedStatement实例
        PreparedStatement ps = this.prepare( sql ) ;

        // 为参数占位符依次设置参数值
        this.setParameter( ps , params );

        try {
            // "执行"SQL语句: 发送标识符和参数值、接收执行结果
            return ps.executeUpdate();
        } catch ( SQLException cause ) {
            // 创建⾃定义异常的实例，并将捕获到的 SQLException 的实例传递给⾃定义异常
            DataAccessException ex = new DataAccessException( "执行 [" + sql + "] 发生错误" , cause ) ;
            // 捕获 SQLException 后，重新抛出⼀个⾃定义异常，以便于通知上层调⽤者
            throw ex ;
        } finally {
            this.close( ps ); // 关闭 PreparedStatement实例
        }
    }

    /**
     * 专门负责执行 DQL语句 ( SELECT )，负责返回单行数据对应的Java对象
     * @param sql 被执行的查询语句
     * @param mapper  用来将查询结果映射成某种类型(T)对象的映射器(mapper)
     * @param params 依次序为带有SQL参数占位符的SQL语句传入相应的参数值
     * @param <T> 用于确定方法所返回的对象类型的 类型参数
     * @return 返回封装了数据库中某行记录对应数据的Java对象
     * @throws DataAccessException
     */
    public <T> T single( String sql , RowMapper<T> mapper , Object... params ) throws DataAccessException {
        T t = null ;
        // 根据 参数传入的SQL语句 创建 PreparedStatement实例
        PreparedStatement ps = this.prepare( sql ) ;

        // 为参数占位符依次设置参数值
        this.setParameter( ps , params );

        ResultSet rs = null ;

        try {
            // "执行"SQL语句: 发送标识符和参数值、接收执行结果
            rs = ps.executeQuery();

            if( rs.next() ) {
                // 将 结果集中的 单行数据 封装到 某个类型 的 Java对象中
                t = mapper.wrap( rs );
            }

        } catch ( SQLException cause ) {
            // 创建⾃定义异常的实例，并将捕获到的 SQLException 的实例传递给⾃定义异常
            DataAccessException ex = new DataAccessException( "查询失败" , cause ) ;
            // 捕获 SQLException 后，重新抛出⼀个⾃定义异常，以便于通知上层调⽤者
            throw ex ;
        } finally {
            this.close( rs ); // 关闭 ResultSet实例
            this.close( ps ); // 关闭 PreparedStatement实例
        }

        return t ;
    }

    /**
     * 专门负责执行 DQL语句 ( SELECT )，负责返回多行数据对应的Java对象组成的List集合
     * @param sql 被执行的查询语句
     * @param mapper 用来将查询结果映射成某种类型(T)对象的映射器(mapper)
     * @param params 依次序为带有SQL参数占位符的SQL语句传入相应的参数值
     * @param <T> 用于确定方法所返回的集合中元素类型的 类型参数
     * @return 返回包含了T类型对象的集合(集合中各个T类型的对象分别封装不同数据库记录对应的数据)
     * @throws DataAccessException
     */
    public <T> List<T> list(String sql , RowMapper<T> mapper, Object... params ) throws DataAccessException {
        List<T> list = new ArrayList<>();
        // 根据 参数传入的SQL语句 创建 PreparedStatement实例
        PreparedStatement ps = this.prepare( sql ) ;

        // 为参数占位符依次设置参数值
        this.setParameter( ps , params );

        ResultSet rs = null ;

        try {
            // "执行"SQL语句: 发送标识符和参数值、接收执行结果
            rs = ps.executeQuery();

            while( rs.next() ) {
                // 将 结果集中的 单行数据 封装到 某个类型 的 Java对象中
                T t = mapper.wrap( rs );
                list.add( t );
            }

        } catch ( SQLException cause ) {
            // 创建⾃定义异常的实例，并将捕获到的 SQLException 的实例传递给⾃定义异常
            DataAccessException ex = new DataAccessException( "查询失败" , cause ) ;
            // 捕获 SQLException 后，重新抛出⼀个⾃定义异常，以便于通知上层调⽤者
            throw ex ;
        } finally {
            this.close( rs ); // 关闭 ResultSet实例
            this.close( ps ); // 关闭 PreparedStatement实例
        }

        return list ;
    }

    /**
     * 专门用于释放资源的方法
     */
    public void release(){
        // 关闭 connection
        this.close( connection );
    }

    // Connection 、Statement 、ResultSet 都继承了 java.lang.AutoCloseable ( JDK 1.7 新增 )
    private void close( AutoCloseable ac ) {
        if( Objects.nonNull( ac ) ) {
            try {
                ac.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    /** 禁用自动提交 */
    private void disable(){
        if( Objects.nonNull( connection ) ) {
            try {
                connection.setAutoCommit(false); // false 表示禁用自动提交
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
    }

    /** 启用自动提交 */
    private void enable(){
        if( Objects.nonNull( connection ) ) {
            try {
                connection.setAutoCommit(true); // true 表示启用自动提交
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
    }

    /** 重新封装事务提交并处理相关异常 */
    private void commit(){
        if( Objects.nonNull( connection ) ) {
            try {
                connection.commit();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
    }

    /** 重新封装回滚提交并处理相关异常 */
    private void rollback(){
        if( Objects.nonNull( connection ) ) {
            try {
                connection.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
    }

    /**
     * 通过 Runner实例 的 doInTransaction 方法来实现在同一个事务中执行多个DML语句
     * @param runner 参数表示封装了用于执行多个DML操作的Java对象
     */
    public void execute( Runner runner ){
        this.disable(); // 禁用自动提交
        this.commit(); // 终结当前会话中之前的事务并开启新的事务
        try {
            runner.doInTransaction();
            this.commit(); // 提交事务
        } catch ( Exception ex ) {
            this.rollback(); // 回滚事务
        }
        this.enable(); // 启用自动提交
    }

}
